Suomi

Mahdollista aito monisäikeisyys JavaScriptissä. Tämä kattava opas käsittelee SharedArrayBufferia, Atomicsia, Web Workereita ja korkean suorituskyvyn verkkosovellusten tietoturvavaatimuksia.

JavaScript SharedArrayBuffer: Syväsukellus verkon rinnakkaisohjelmointiin

Vuosikymmenten ajan JavaScriptin yksisäikeinen luonne on ollut sekä sen yksinkertaisuuden lähde että merkittävä suorituskyvyn pullonkaula. Tapahtumasilmukkamalli toimii kauniisti useimmissa käyttöliittymäkeskeisissä tehtävissä, mutta se kohtaa haasteita laskennallisesti raskaiden operaatioiden kanssa. Pitkäkestoiset laskutoimitukset voivat jäädyttää selaimen, mikä luo turhauttavan käyttökokemuksen. Vaikka Web Workerit tarjosivat osittaisen ratkaisun sallimalla skriptien suorittamisen taustalla, niillä oli oma merkittävä rajoituksensa: tehoton datanvälitys.

Tässä astuu kuvaan SharedArrayBuffer (SAB), tehokas ominaisuus, joka muuttaa pelin perusteellisesti tuomalla aidon, matalan tason muistin jakamisen verkon säikeiden välille. Yhdessä Atomics-olion kanssa SAB avaa uuden aikakauden korkean suorituskyvyn rinnakkaissovelluksille suoraan selaimessa. Suuren voiman myötä tulee kuitenkin suuri vastuu – ja monimutkaisuus.

Tämä opas vie sinut syväsukellukselle JavaScriptin rinnakkaisohjelmoinnin maailmaan. Tutkimme, miksi tarvitsemme sitä, miten SharedArrayBuffer ja Atomics toimivat, kriittiset tietoturvanäkökohdat, jotka sinun on otettava huomioon, sekä käytännön esimerkkejä, joiden avulla pääset alkuun.

Vanha maailma: JavaScriptin yksisäikeinen malli ja sen rajoitukset

Ennen kuin voimme arvostaa ratkaisua, meidän on ymmärrettävä ongelma täysin. JavaScriptin suoritus selaimessa tapahtuu perinteisesti yhdellä säikeellä, jota kutsutaan usein "pääsäikeeksi" tai "käyttöliittymäsäikeeksi".

Tapahtumasilmukka

Pääsäie on vastuussa kaikesta: JavaScript-koodisi suorittamisesta, sivun renderöinnistä, käyttäjän vuorovaikutuksiin (kuten klikkauksiin ja vierityksiin) vastaamisesta sekä CSS-animaatioiden ajamisesta. Se hallitsee näitä tehtäviä tapahtumasilmukan avulla, joka käsittelee jatkuvasti viestijonoa (tehtäviä). Jos tehtävän suorittaminen kestää kauan, se estää koko jonon toiminnan. Mitään muuta ei voi tapahtua – käyttöliittymä jäätyy, animaatiot pätkivät ja sivu muuttuu reagoimattomaksi.

Web Workerit: Askel oikeaan suuntaan

Web Workerit otettiin käyttöön tämän ongelman lievittämiseksi. Web Worker on käytännössä skripti, joka suoritetaan erillisessä taustasäikeessä. Voit siirtää raskaita laskutoimituksia workerille, jolloin pääsäie pysyy vapaana käsittelemään käyttöliittymää.

Viestintä pääsäikeen ja workerin välillä tapahtuu postMessage()-API:n kautta. Kun lähetät dataa, se käsitellään strukturoidun kloonauksen algoritmilla. Tämä tarkoittaa, että data sarjallistetaan, kopioidaan ja sitten deserialisoidaan workerin kontekstissa. Vaikka tämä on tehokasta, prosessilla on merkittäviä haittoja suurten datajoukkojen kohdalla:

Kuvittele videoeditori selaimessa. Koko videokehyksen (joka voi olla useita megatavuja) lähettäminen edestakaisin workerille käsiteltäväksi 60 kertaa sekunnissa olisi kohtuuttoman kallista. Tämä on juuri se ongelma, jonka SharedArrayBuffer suunniteltiin ratkaisemaan.

Pelin muuttaja: SharedArrayBufferin esittely

SharedArrayBuffer on kiinteän pituinen raaka binääridatapuskuri, samankaltainen kuin ArrayBuffer. Kriittinen ero on, että SharedArrayBuffer voidaan jakaa useiden säikeiden (esim. pääsäikeen ja yhden tai useamman Web Workerin) kesken. Kun "lähetät" SharedArrayBufferin käyttämällä postMessage()-metodia, et lähetä kopiota; lähetät viittauksen samaan muistilohkoon.

Tämä tarkoittaa, että kaikki yhden säikeen puskurin dataan tekemät muutokset ovat välittömästi näkyvissä kaikille muille säikeille, joilla on viittaus siihen. Tämä eliminoi kalliin kopiointi- ja sarjallistamisvaiheen, mahdollistaen lähes välittömän datan jakamisen.

Ajattele sitä näin:

Jaetun muistin vaara: Kilpailutilanteet

Välitön muistin jakaminen on tehokasta, mutta se tuo mukanaan myös klassisen ongelman rinnakkaisohjelmoinnin maailmasta: kilpailutilanteet (race conditions).

Kilpailutilanne syntyy, kun useat säikeet yrittävät käyttää ja muokata samaa jaettua dataa samanaikaisesti, ja lopputulos riippuu niiden arvaamattomasta suoritusjärjestyksestä. Kuvitellaan yksinkertainen laskuri, joka on tallennettu SharedArrayBufferiin. Sekä pääsäie että workeri haluavat kasvattaa sitä.

  1. Säie A lukee nykyisen arvon, joka on 5.
  2. Ennen kuin säie A ehtii kirjoittaa uutta arvoa, käyttöjärjestelmä keskeyttää sen ja vaihtaa säikeeseen B.
  3. Säie B lukee nykyisen arvon, joka on edelleen 5.
  4. Säie B laskee uuden arvon (6) ja kirjoittaa sen takaisin muistiin.
  5. Järjestelmä vaihtaa takaisin säikeeseen A. Se ei tiedä, että säie B teki mitään. Se jatkaa siitä, mihin se jäi, laskien oman uuden arvonsa (5 + 1 = 6) ja kirjoittaen arvon 6 takaisin muistiin.

Vaikka laskuria kasvatettiin kahdesti, lopullinen arvo on 6, ei 7. Operaatiot eivät olleet atomisia – ne olivat keskeytettävissä, mikä johti datan katoamiseen. Juuri tästä syystä et voi käyttää SharedArrayBufferia ilman sen elintärkeää kumppania: Atomics-oliota.

Jaetun muistin vartija: Atomics-olio

Atomics-olio tarjoaa joukon staattisia metodeja atomisten operaatioiden suorittamiseen SharedArrayBuffer-olioilla. Atominen operaatio suoritetaan taatusti kokonaisuudessaan ilman, että mikään muu operaatio keskeyttää sitä. Se joko tapahtuu kokonaan tai ei lainkaan.

Atomicsin käyttö estää kilpailutilanteet varmistamalla, että luku-muokkaus-kirjoitus-operaatiot jaetussa muistissa suoritetaan turvallisesti.

Tärkeimmät Atomics-metodit

Katsotaanpa joitakin tärkeimpiä Atomicsin tarjoamia metodeja.

Synkronointi: Yksinkertaisia operaatioita pidemmälle

Joskus tarvitset enemmän kuin vain turvallista lukemista ja kirjoittamista. Säikeiden on koordinoitava ja odotettava toisiaan. Yleinen anti-pattern on "aktiivinen odotus" (busy-waiting), jossa säie istuu tiukassa silmukassa tarkistaen jatkuvasti muistipaikkaa muutoksen varalta. Tämä tuhlaa CPU-syklejä ja kuluttaa akkua.

Atomics tarjoaa paljon tehokkaamman ratkaisun metodeilla wait() ja notify().

Kaiken yhdistäminen: Käytännön opas

Nyt kun ymmärrämme teorian, käydään läpi vaiheet ratkaisun toteuttamiseksi käyttämällä SharedArrayBufferia.

Vaihe 1: Tietoturvan ennakkoehto - Cross-Origin-eristys

Tämä on yleisin kompastuskivi kehittäjille. Tietoturvasyistä SharedArrayBuffer on saatavilla vain sivuilla, jotka ovat cross-origin-eristetyssä tilassa. Tämä on turvatoimenpide spekulatiivisen suorituksen haavoittuvuuksien, kuten Spectren, lieventämiseksi, jotka voisivat mahdollisesti käyttää korkean resoluution ajastimia (jaetun muistin mahdollistamana) vuotaakseen dataa alkuperien välillä.

Ottaksesi cross-origin-eristyksen käyttöön, sinun on määritettävä verkkopalvelimesi lähettämään kaksi tiettyä HTTP-otsaketta päädokumentillesi:


Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Tämän määrittäminen voi olla haastavaa, erityisesti jos käytät kolmannen osapuolen skriptejä tai resursseja, jotka eivät tarjoa tarvittavia otsakkeita. Palvelimen konfiguroinnin jälkeen voit varmistaa, onko sivusi eristetty, tarkistamalla self.crossOriginIsolated-ominaisuuden selaimen konsolista. Sen on oltava true.

Vaihe 2: Puskurin luominen ja jakaminen

Pääskriptissäsi luot SharedArrayBufferin ja sille "näkymän" käyttämällä TypedArray:ta, kuten Int32Array.

main.js:


// Check for cross-origin isolation first!
if (!self.crossOriginIsolated) {
  console.error("This page is not cross-origin isolated. SharedArrayBuffer will not be available.");
} else {
  // Create a shared buffer for one 32-bit integer.
  const buffer = new SharedArrayBuffer(4);

  // Create a view on the buffer. All atomic operations happen on the view.
  const int32Array = new Int32Array(buffer);

  // Initialize the value at index 0.
  int32Array[0] = 0;

  // Create a new worker.
  const worker = new Worker('worker.js');

  // Send the SHARED buffer to the worker. This is a reference transfer, not a copy.
  worker.postMessage({ buffer });

  // Listen for messages from the worker.
  worker.onmessage = (event) => {
    console.log(`Worker reported completion. Final value: ${Atomics.load(int32Array, 0)}`);
  };
}

Vaihe 3: Atomisten operaatioiden suorittaminen workerissa

Workeri vastaanottaa puskurin ja voi nyt suorittaa sille atomisia operaatioita.

worker.js:


self.onmessage = (event) => {
  const { buffer } = event.data;
  const int32Array = new Int32Array(buffer);

  console.log("Worker received the shared buffer.");

  // Let's perform some atomic operations.
  for (let i = 0; i < 1000000; i++) {
    // Safely increment the shared value.
    Atomics.add(int32Array, 0, 1);
  }

  console.log("Worker finished incrementing.");

  // Signal back to the main thread that we are done.
  self.postMessage({ done: true });
};

Vaihe 4: Edistyneempi esimerkki - Rinnakkainen summaus synkronoinnilla

Käsitelläänpä realistisempaa ongelmaa: erittäin suuren numerotaulukon summaamista käyttämällä useita workereita. Käytämme Atomics.wait() ja Atomics.notify() tehokkaaseen synkronointiin.

Jaetussa puskurissamme on kolme osaa:

main.js:


if (self.crossOriginIsolated) {
  const NUM_WORKERS = 4;
  const DATA_SIZE = 10_000_000;

  // [status, workers_finished, result_low, result_high]
  // We use two 32-bit integers for the result to avoid overflow for large sums.
  const sharedBuffer = new SharedArrayBuffer(4 * 4); // 4 integers
  const sharedArray = new Int32Array(sharedBuffer);

  // Generate some random data to process
  const data = new Uint8Array(DATA_SIZE);
  for (let i = 0; i < DATA_SIZE; i++) {
    data[i] = Math.floor(Math.random() * 10);
  }

  const chunkSize = Math.ceil(DATA_SIZE / NUM_WORKERS);

  for (let i = 0; i < NUM_WORKERS; i++) {
    const worker = new Worker('sum_worker.js');
    const start = i * chunkSize;
    const end = Math.min(start + chunkSize, DATA_SIZE);
    
    // Create a non-shared view for the worker's chunk of data
    const dataChunk = data.subarray(start, end);

    worker.postMessage({ 
      sharedBuffer,
      dataChunk // This is copied
    });
  }

  console.log('Main thread is now waiting for workers to finish...');

  // Wait for the status flag at index 0 to become 1
  // This is much better than a while loop!
  Atomics.wait(sharedArray, 0, 0); // Wait if sharedArray[0] is 0

  console.log('Main thread woken up!');
  const finalSum = Atomics.load(sharedArray, 2);
  console.log(`The final parallel sum is: ${finalSum}`);

} else {
  console.error('Page is not cross-origin isolated.');
}

sum_worker.js:


self.onmessage = ({ data }) => {
  const { sharedBuffer, dataChunk } = data;
  const sharedArray = new Int32Array(sharedBuffer);

  // Calculate the sum for this worker's chunk
  let localSum = 0;
  for (let i = 0; i < dataChunk.length; i++) {
    localSum += dataChunk[i];
  }

  // Atomically add the local sum to the shared total
  Atomics.add(sharedArray, 2, localSum);

  // Atomically increment the 'workers finished' counter
  const finishedCount = Atomics.add(sharedArray, 1, 1) + 1;

  // If this is the last worker to finish...
  const NUM_WORKERS = 4; // Should be passed in a real app
  if (finishedCount === NUM_WORKERS) {
    console.log('Last worker finished. Notifying main thread.');

    // 1. Set the status flag to 1 (complete)
    Atomics.store(sharedArray, 0, 1);

    // 2. Notify the main thread, which is waiting on index 0
    Atomics.notify(sharedArray, 0, 1);
  }
};

Tosimaailman käyttötapaukset ja sovellukset

Missä tämä tehokas mutta monimutkainen teknologia todella tekee eron? Se loistaa sovelluksissa, jotka vaativat raskasta, rinnakkaistettavaa laskentaa suurilla datajoukoilla.

Haasteet ja loppuhuomiot

Vaikka SharedArrayBuffer on mullistava, se ei ole ihmelääke. Se on matalan tason työkalu, joka vaatii huolellista käsittelyä.

  1. Monimutkaisuus: Rinnakkaisohjelmointi on tunnetusti vaikeaa. Kilpailutilanteiden ja lukkiutumien (deadlocks) debuggaus voi olla uskomattoman haastavaa. Sinun on ajateltava eri tavalla sovelluksesi tilan hallinnasta.
  2. Lukkiutumat: Lukkiutuma tapahtuu, kun kaksi tai useampi säie on pysyvästi estettynä, odottaen toisiaan vapauttamaan resurssin. Tämä voi tapahtua, jos toteutat monimutkaisia lukitusmekanismeja virheellisesti.
  3. Tietoturvan lisävaatimukset: Cross-origin-eristysvaatimus on merkittävä este. Se voi rikkoa integraatioita kolmannen osapuolen palveluihin, mainoksiin ja maksuyhdyskäytäviin, jos ne eivät tue tarvittavia CORS/CORP-otsakkeita.
  4. Ei jokaiseen ongelmaan: Yksinkertaisiin taustatehtäviin tai I/O-operaatioihin perinteinen Web Worker -malli postMessage():lla on usein yksinkertaisempi ja riittävä. Ota SharedArrayBuffer käyttöön vain, kun sinulla on selkeä, CPU-sidonnainen pullonkaula, joka liittyy suuriin datamääriin.

Yhteenveto

SharedArrayBuffer yhdessä Atomicsin ja Web Workereiden kanssa edustaa paradigman muutosta web-kehityksessä. Se murtaa yksisäikeisen mallin rajat, kutsuen uuden luokan tehokkaita, suorituskykyisiä ja monimutkaisia sovelluksia selaimeen. Se asettaa verkkoympäristön tasavertaisempaan asemaan natiivisovelluskehityksen kanssa laskennallisesti raskaissa tehtävissä.

Matka rinnakkaiseen JavaScriptiin on haastava, vaatien kurinalaista lähestymistapaa tilanhallintaan, synkronointiin ja tietoturvaan. Mutta kehittäjille, jotka haluavat ylittää verkon mahdollisuuksien rajoja – reaaliaikaisesta äänisynteesistä monimutkaiseen 3D-renderöintiin ja tieteelliseen laskentaan – SharedArrayBufferin hallitseminen ei ole enää vain vaihtoehto; se on olennainen taito seuraavan sukupolven verkkosovellusten rakentamisessa.